(function () {


'use strict'


// functions that can be evaluated for rendering. (URL about: http://underscorejs.org/#template)
const tmplFuncs = new Map();
tmplFuncs.set('login', _.template($('#login-tmpl').html()));
tmplFuncs.set('manager', _.template($('#manager-tmpl').html()));
tmplFuncs.set('home', _.template($('#home-tmpl').html()));
tmplFuncs.set('apps', _.template($('#apps-tmpl').html()));
tmplFuncs.set('reports', _.template($('#reports-tmpl').html()));
tmplFuncs.set('user', _.template($('#user-tmpl').html()));
tmplFuncs.set('not-found', _.template($('#not-found-tmpl').html()));

const startButtonTemplate = '<button data-role="start-app" data-app-path="<%= app.path %>" type="button" class="btn btn-light btn-sm mr-1">启动</button>';
const stopButtonTemplate = '<button data-role="stop-app" data-app-path="<%= app.path %>" type="button" class="btn btn-light btn-sm mr-1">停止</button>';

const htmls = new Map();
htmls.set('loading-indicator', '<i> </i><i class="fa fa-circle-o-notch fa-spin"></i>');
htmls.set('sr-only-current', '<span class="sr-only">(current)</span>');
htmls.set('pause-button', '<button data-role="pause-rf-memory" type="button" class="btn btn-sm btn-outline-secondary" title="暂停"><i class="fa fa-pause"></i></button>');
htmls.set('resume-button', '<button data-role="resume-rf-memory" type="button" class="btn btn-sm btn-outline-secondary" title="继续"><i class="fa fa-play"></i></button>');
htmls.set('show-password', '<span data-role="show-password" data-feather="eye-off" class="pwd-toggle"></span>');
htmls.set('hide-password', '<span data-role="hide-password" data-feather="eye" class="pwd-toggle"></span>');

const chartColors = {
  red:    'rgb(255,  99, 132)',
  orange: 'rgb(255, 159,  64)',
  yellow: 'rgb(255, 205,  86)',
  green:  'rgb( 75, 192, 192)',
  blue:   'rgb( 54, 162, 235)',
  purple: 'rgb(153, 102, 255)',
  grey:   'rgb(201, 203, 207)'
};


// Router --------------------------------------------------------------------------------------------------------------------------------------------------------------------

const router = {

  lastRequestedFragment: '',

  history: {
    fragment: ''
  },

  route: function (fragment)
  {
    // works only when child view of main view is 'manager'
    if (mainView.childView.id === 'manager')
    {
      if (!this.routes.has(fragment))
      {
        this.requestView('not-found');
        this.history.fragment = 'not-found';
        window.history.replaceState(null, '', window.location.pathname + '#not-found');
      }
      else
      {
        const viewId = this.routes.get(fragment);
        this.requestView(viewId);
        this.history.fragment = fragment;
      }
    }
    else
    {
      this.lastRequestedFragment = fragment;
      window.history.pushState(null, '', window.location.pathname + '#');
      throw 'Please sign in';
    }
  },

  routes: new Map(
  [
    ['', 'home'],
    ['home', 'home'],
    ['reports', 'reports'],
    ['user', 'user']
  ]),

  requestView: function (id)
  {
    const container = mainView.childView;
    const childViewId = container.getChildViewId();
    if (childViewId !== 'not-found' && childViewId == id)
    {
      // do nothing to reserve current child view
    }
    else
    {
      container.showView(id);
    }
  }
};


// Handler corresponding to the router ---------------------------------------------------------------------------------------------------------------------------------------

const handleHashChange = function ()
{
  const fragment = getHash();
  if (fragment !== router.history.fragment)
  {
    try
    {
      router.route(fragment);
    }
    catch (e)
    {
      alert(e);
    }
  }
};


// Views ---------------------------------------------------------------------------------------------------------------------------------------------------------------------

const mainView = {
  
  $el: $("#app-main"),

  childView: null,

  transitionTo: function (view, data, callback)
  {
    if (this.childView)
    {
      const self = this;
      this.childView.fadeOut(function ()
      {
        self.appendAndRender(view, data, callback);
      });
    }
    else
    {
      this.appendAndRender(view, data, callback);
    }
    this.childView = view;
  },

  appendAndRender: function (view, data, callback)
  {
    this.$el.empty().append(view.$el);
    view.fadeIn(data, function ()
    {
      _.delay(function ()
      {
        view.delegateEvents();
        if ('postRender' in view)
        {
          view.postRender();
        }
        if (callback)
        {
          callback();
        }
      }, 20);
    });
    feather.replace();
  }
};

class BaseChildView
{
  constructor(id)
  {
    this.id = id;
    this.$el = $('<div></div>');
    this.tmpl = tmplFuncs.get(id);
  }

  $(selector)
  {
    return this.$el.find(selector);
  }

  fadeIn(data, callback)
  {
    this.$el
      .css('display', 'none')
      .html(this.tmpl(data))
      .fadeIn(400, function ()
      {
        if (callback)
        {
          callback();
        }
      });
  }

  fadeOut(callback)
  {
    this.$el.fadeOut(400, function ()
    {
      if (callback)
      {
        callback();
      }
    });
  }

  delegateEvents()
  {
  }

  cleanup()
  {
  }
};

class LoginView extends BaseChildView
{
  constructor()
  {
    super('login');
  }

  postRender()
  {
    this.$('#username').focus();
  }

  delegateEvents()
  {
    const self = this;
    this.$('#form-login').submit(function (event)
    {
      self.login(event);
    });
  }

  /* Event handler(s) */

  login(event)
  {
    event.preventDefault();
    const $current = $(event.currentTarget);
    const $submit = $current.find('#submit');
    const $username = $current.find('#username');
    const $password = $current.find('#password');
    appendLoadingIndicator($submit);
    const user = {
      username: $username.val(),
      password: $password.val()
    };
    $.when(delay(1000))
    .done(function ()
    {
      if (user.username !== "admin" || user.password !== "example")
      {
        alert('用户名或密码错误');
        $password.val('');
      }
      else
      {
        user.password = '[PROTECTED]';
        security.user = user;
        mainView.transitionTo(new ManagerView(), user);
      }
    })
    .fail(function (jqXHR, textStatus, errorThrown)
    {
      // ...
    })
    .always(function ()
    {
      resetElement($submit);
    });
  }
};

class ManagerView extends BaseChildView
{
  $disabledTriggers = null;
  childView = null;
  cachedViews = null;
  childViewConstructors = null;

  constructor()
  {
    super('manager');
    this.cachedViews = new Map();
    this.childViewConstructors = new Map();
    this.childViewConstructors.set('home', HomeView);
    this.childViewConstructors.set('reports', ReportsView);
    this.childViewConstructors.set('user', UserView);
    this.childViewConstructors.set('not-found', NotFoundView);
  }

  fadeIn(user, callback)
  {
    super.fadeIn(user, function ()
    {
      if (getHash() === router.lastRequestedFragment)
      {
        router.route(router.lastRequestedFragment);
      }
      window.location.hash = router.lastRequestedFragment;
      if (callback)
      {
        callback();
      }
    });
  }

  showView(id)
  {
    const view = this.getChildView(id);
    if (view)
    {
      if ('loadData' in view)
      {
        this.disableTriggers();
        this.notifyLoading(view.id);
        const self = this;
        this.performLoadData(view, {
          success: function (data)
          {
            self.transitionTo(view, data);
          },
          fail: function (jqXHR)
          {
            self.transitionTo(getErrorView(), jqXHR);
          },
          always: function ()
          {
            self.enableTriggers();
          }
        });
      }
      else
      {
        this.transitionTo(view);
      }
    }
    else
    {
      throw 'Failed to get view \'' + id + '\'';
    }
  }

  disableTriggers()
  {
    this.$disabledTriggers = this.$('.div-trigger').filter(function ()
    {
      return !$(this).hasClass('disabled') && !$(this).prop('disabled');
    });
    this.$disabledTriggers.prop('disabled', true).addClass('disabled');
  }

  enableTriggers()
  {
    if (this.$disabledTriggers)
    {
      this.$disabledTriggers.removeClass('disabled').prop('disabled', false);
    }
  }

  transitionTo(view, data)
  {
    if (this.childView)
    {
      this.preserveHeight();
      const self = this;
      this.childView.fadeOut(function ()
      {
        self.inactivateAnchors();
        self.activateAnchor(view.id);
        self.childView.cleanup();
        self.appendAndRender(view, data);
      });
    }
    else
    {
      this.activateAnchor(view.id);
      this.appendAndRender(view, data);
    }
  }

  inactivateAnchors()
  {
    const $anchors = this.$('[data-role="route-anchor"]');
    $anchors.removeClass('active');
    const $srOnlys = $anchors.find('.sr-only');
    $srOnlys.remove();
  }

  activateAnchor(id)
  {
    const $anchor = this.getAnchor(id);
    if ($anchor)
    {
      $anchor.addClass('active');
      $anchor.append(htmls.get('sr-only-current'));
    }
  }

  preserveHeight()
  {
    const height = this.$('#container').css('height');
    this.$('#container').css('height', height);
  }

  performLoadData(view, callbacks)
  {
    const $anchor = this.getAnchor(view.id);
    const self = this;
    $.when(view.loadData(), delay(1000)) // TODO: remove delay(1000)
    .done(function (data)
    {
      try
      {
        callbacks.success(data);
      }
      catch (e)
      {
        alert(e);
        resetElement($anchor);
        throw e;
      }
    })
    .fail(function (jqXHR)
    {
      callbacks.fail(jqXHR);
    })
    .always(function ()
    {
      resetElement($anchor);
      callbacks.always();
    });
  }

  getAnchor(viewId)
  {
    switch (viewId)
    {
      case 'home':
        return this.$('#home-anchor');
      case 'reports':
        return this.$('#reports-anchor');
      case 'threads':
        return this.$('#threads-anchor');
      case 'user':
        return this.$('#user-anchor');
    }
  }

  notifyLoading(viewId)
  {
    let $anchor = this.getAnchor(viewId);
    if ($anchor)
    {
      appendLoadingIndicator($anchor);
    }
    return $anchor;
  }

  appendAndRender(view, data)
  {
    this.$('#container').empty().append(view.$el);
    this.removePreservedHeight();
    view.fadeIn(data, function ()
    {
      _.delay(function ()
      {
        view.delegateEvents();
        if ('postRender' in view)
        {
          view.postRender(data);
        }
      }, 20);
    });
    feather.replace(); // should before delegate events, since some events may trigger by svgs generated from feather_icon
    this.childView = view;
  }

  removePreservedHeight()
  {
    this.$('#container').css('height', '');
  }

  getChildView(id)
  {
    if (this.cachedViews.has(id))
    {
      return this.cachedViews.get(id);
    }
    else
    {
      if (!this.childViewConstructors.has(id))
      {
        return null;
      }
      else
      {
        const constructor = this.childViewConstructors.get(id);
        return new constructor();
      }
    }
  }

  getChildViewId()
  {
    return this.childView ? this.childView.id : null;
  }

  delegateEvents()
  {
    const self = this;
    this.$('#logout').click(function ()
    {
      self.logout();
    });
  }

  /* Event handler(s) */

  logout(event)
  {
    security.logout();
    if (this.childView)
    {
      this.childView.cleanup();
    }
    mainView.transitionTo(new LoginView(), null, function ()
    {
      $('#username').blur();
    });
    router.lastRequestedFragment = '';
    router.history.fragment = '';
    window.history.pushState(null, '', window.location.pathname + '#');
  }
}

class HomeView extends BaseChildView
{
  constructor()
  {
    super('home');
    this.memoryChart = null;
    this.timers = {};
    this.maxMemory = null;
    this.initialMemory = null;
  }

  fadeIn(data, callback)
  {
    const self = this;
    super.fadeIn(data, function ()
    {
      if (callback)
      {
        callback();
      }
      self.showMemoryChart(data.memoryUsage);
    });
  }

  loadData()
  {
    const deferred = $.Deferred();
    deferred.resolve({
      apps: [
        { path: "", name: "ROOT", isRunning: "true", sessionNum: 1 },
        { path: "test", name: "Test", isRunning: "true", sessionNum: 31 },
        { path: "dev", name: "Dev", isRunning: "false", sessionNum: 0 }
      ],
      serverInfo: {},
      memoryUsage: {maxMemory: 9663676416, initialMemory: 1073741824, totalMemory: 1073741824, freeMemory: 858993459}
    });
    return deferred.promise();
  }

  postRender(data)
  {
    this.addTimer('refresh-memory', this.newRefreshMemoryTimer());
    bsCustomFileInput.init();
  }

  delegateEvents()
  {
    const self = this;
    // app table
    this.$('#rf-apps').click(function (event)
    {
      self.refreshApps(event);
    });
    // app commands
    this.$('button[data-role="start-app"]').click(function (event)
    {
      self.startApp(event);
    });
    this.$('button[data-role="stop-app"]').click(function (event)
    {
      self.stopApp(event);
    });
    this.$('button[data-role="reload-app"]').click(function (event)
    {
      self.reloadApp(event);
    });
    this.$('button[data-role="remove-app"]').click(function (event)
    {
      self.removeApp(event);
    });
    this.$('button[data-role="expire-sessions"]').click(function (event)
    {
      self.expireSessions(event);
    });
    // add app
    this.$('#add-app').submit(function (event)
    {
      self.addApp(event)
    });
    this.$('#app-zip').change(function (event)
    {
      $(this).removeClass('is-invalid');
    });
    // memory
    this.$('button[data-role="pause-rf-memory"]').click(function (event)
    {
      self.pauseRefreshMemory(event);
    });
  }

  /* Event handler(s) */

  refreshApps(event)
  {
    const $button = $(event.currentTarget);
    const content = $button.html();
    toLoadingIndicator($button);
    const self = this;
    $.when(delay(1000))
    .done(function ()
    {
      const $table = self.$('#app-table');
      const $tbody = $table.find('tbody');
      $tbody.fadeOut(400, function ()
      {
        const $newTbody = $('<tbody></tbody>');
        $newTbody.css('display', 'none');
        const appsTmplFunc = tmplFuncs.get('apps');
        $newTbody.html(appsTmplFunc({
            apps: [
              { path: "", name: "ROOT", isRunning: "true", sessionNum: 1 },
              { path: "test", name: "Test", isRunning: "true", sessionNum: _.random(10000) },
              { path: "dev", name: "Dev", isRunning: "false", sessionNum: 0 }
            ]}));
        $tbody.remove();
        $table.append($newTbody);
        $newTbody.fadeIn(400, function ()
        {
          self.delegateCommands($newTbody);
        });
      });
    })
    .fail(function ()
    {
    })
    .always(function ()
    {
      resetButton($button, content);
    });
  }

  stopApp(event)
  {
    const $button = $(event.currentTarget);
    const content = $button.html();
    toLoadingIndicator($button);
    const path = $button.data('app-path');
    const self = this;
    $.when(delay(1000))
    .done(function ()
    {
      const $parent = $button.parent();
      const startButtonTmpl = _.template(startButtonTemplate);
      const apps = [ { path: "", sessionNum: 0 }, { path: "test", sessionNum: 0 }, { path: "dev", sessionNum: 0 } ];
      for (let i = 0; i < apps.length; i ++)
      {
        let app = apps[i];
        if (app.path === path)
        {
          app.isRunning = "false";
          const $startButton = $(startButtonTmpl({app: app}));
          $startButton.css('display', 'none');
          $button.fadeOut(400, function ()
          {
            $button.remove();
            $parent.prepend($startButton);
            $startButton.fadeIn(400, function ()
            {
              self.delegateStartApp($startButton);
            });
          });
          const $tr = $parent.closest('tr');
          self.updateAppState($tr, app);
          self.updateAppSessionNum($tr, app);
        }
      }
    })
    .fail(function (jqXHR, textStatus, errorThrown)
    {
      if (!handledGlobally(jqXHR.status))
      {
        if (jqXHR.responseText)
        {
          alert(jqXHR.responseText);
        }
        else
        {
          alert('Failed to stop app:\n    ' + errorThrown);
        }
      }
      resetButton($button, content);
    });
  }

  startApp(event)
  {
    const $button = $(event.currentTarget);
    const content = $button.html();
    toLoadingIndicator($button);
    const path = $button.data('app-path');
    const self = this;
    $.when(delay(1000))
    .done(function ()
    {
      const $parent = $button.parent();
      const stopButtonTmpl = _.template(stopButtonTemplate);
      const apps = [ { path: "" }, { path: "test" }, { path: "dev" } ];
      for (let i = 0; i < apps.length; i ++)
      {
        let app = apps[i];
        if (app.path === path)
        {
          app.isRunning = "true";
          const $stopButton = $(stopButtonTmpl({app: app}));
          $stopButton.css('display', 'none');
          $button.fadeOut(400, function ()
          {
            $button.remove();
            $parent.prepend($stopButton);
            $stopButton.fadeIn(400, function ()
            {
              self.delegateStopApp($stopButton);
            });
          });
          const $tr = $parent.closest('tr');
          self.updateAppState($tr, app);
        }
      }
    })
    .fail(function (jqXHR, textStatus, errorThrown)
    {
      if (!handledGlobally(jqXHR.status))
      {
        if (jqXHR.responseText)
        {
          alert(jqXHR.responseText);
        }
        else
        {
          alert('Failed to start app:\n    ' + errorThrown);
        }
      }
      resetButton($button, content);
    });
  }

  reloadApp(event)
  {
    const $button = $(event.currentTarget);
    const content = $button.html();
    toLoadingIndicator($button);
    const path = $button.data('app-path');
    const self = this;
    $.when(sendJson('reload-app', { path: path }))
    .done(function (app)
    {
      const $tr = $button.closest('tr');
      self.updateAppInfo($tr, app);
      self.resetToStopButton($tr, app);
    })
    .fail(function (jqXHR, textStatus, errorThrown)
    {
      if (!handledGlobally(jqXHR.status))
      {
        if (jqXHR.responseText)
        {
          alert(jqXHR.responseText);
        }
        else
        {
          alert('Failed to reload app:\n    ' + errorThrown);
        }
      }
    })
    .always(function ()
    {
      resetButton($button, content);
    });
  }

  removeApp(event)
  {
    const $button = $(event.currentTarget);
    const path = $button.data('app-path');
    const sure = confirm('Sure to remove app with path \'' + path + '\'?');
    if (sure)
    {
      const content = $button.html();
      toLoadingIndicator($button);
      const self = this;
      $.when(sendJson('remove-app', { path: path }))
      .done(function (data, textStatus, jqXHR)
      {
        const $tr = $button.closest('tr');
        $tr.fadeOut(400, function ()
        {
          $tr.remove();
        });
      })
      .fail(function (jqXHR, textStatus, errorThrown)
      {
        if (!handledGlobally(jqXHR.status))
        {
          if (jqXHR.responseText)
          {
            alert(jqXHR.responseText);
          }
          else
          {
            alert('Failed to remove app:\n    ' + errorThrown);
          }
        }
        resetButton($button, content);
      });
    }
  }

  expireSessions(event)
  {
    const $button = $(event.currentTarget);
    const content = $button.html();
    toLoadingIndicator($button);
    const path = $button.data('app-path');
    const $inputGroup = $button.closest('.input-group');
    const $input = $inputGroup.find('input');
    const idle = Number($input.val());
    const self = this;
    $.when(sendJson('expire-sessions', { path: path, idle: idle }))
    .done(function (app)
    {
      const $tr = $button.closest('tr');
      const $sessions = $tr.find('div[data-role="display-sessions"]');
      const num = Number($sessions.html()) - app.sessionNum;
      alert('Sessions expired: ' + num);
      self.updateAppSessionNum($tr, app);
    })
    .fail(function (jqXHR, textStatus, errorThrown)
    {
      if (!handledGlobally(jqXHR.status))
      {
        if (jqXHR.status === 400)
        {
          alert(jqXHR.responseText);
        }
        else
        {
          alert('Failed to expire sessions:\n    ' + errorThrown);
        }
      }
    })
    .always(function ()
    {
      resetButton($button, content);
    });
  }

  addApp(event)
  {
    event.preventDefault();
    const $form = $(event.currentTarget);
    const $fieldset = $form.find('fieldset');
    $fieldset.prop('disabled', true);
    const $file = $form.find('#app-zip');
    const file = $file[0].files[0];
    if (!file)
    {
      alert('请先选择一个应用打包文件（zip 格式）');
      $fieldset.prop('disabled', false);
      this.pulseFile($file);
    }
    else
    {
      if (file.type !== 'application/zip')
      {
        alert('只接受 zip 格式的应用打包文件');
        $fieldset.prop('disabled', false);
        this.pulseFile($file);
      }
      else
      {
        const self = this;
        const data = new FormData();
        data.append('file', file);
        const $submit = $form.find('#add-app-submit');
        const content = $submit.html();
        const $progress = $form.find('progress');
        $progress.fadeTo(150, 1);
        toLoadingIndicator($submit);
        // reference: https://stackoverflow.com/questions/166221/how-can-i-upload-files-asynchronously
        $.ajax({
          url: 'add-app',
          type: 'POST',
          data: data,
          // tell JQ not to process data or worry about content-type
          // *must* include these options!
          cache: false,
          contentType: false,
          processData: false,
          // custom XMLHttpRequest
          xhr: function ()
          {
            const myXhr = $.ajaxSettings.xhr();
            if (myXhr.upload)
            {
              // for handling the progress of the upload
              myXhr.upload.addEventListener('progress', function (event)
              {
                if (event.lengthComputable)
                {
                  $progress.prop('value', event.loaded).prop('max', event.total);
                }
              }, false);
            }
            return myXhr;
          }
        })
        .done(function (data, textStatus, jqXHR)
        {
          self.$('#rf-apps').trigger('click');
        })
        .fail(function (jqXHR, textStatus, errorThrown)
        {
          if (!handledGlobally(jqXHR.status))
          {
            if (jqXHR.status === 400)
            {
              alert(jqXHR.responseText);
            }
            else
            {
              alert('Failed to add app:\n    ' + errorThrown);
            }
          }
        })
        .always(function ()
        {
          $fieldset.prop('disabled', false);
          resetButton($submit, content);
          $progress.fadeTo(150, 0, function ()
          {
            $progress.prop('value', 0);
          });
        });
      }
    }
  }

  pauseRefreshMemory(event)
  {
    const $button = $(event.currentTarget);
    $button.prop('disabled', true);
    const $parent = $button.parent();
    this.removeTimer('refresh-memory');
    const $resumeButton = $(htmls.get('resume-button'));
    $resumeButton.css('display', 'none');
    const self = this;
    $button.fadeOut(150, function()
    {
      $button.remove();
      $parent.append($resumeButton);
      feather.replace();
      $resumeButton.fadeIn(150, function()
      {
        $resumeButton.click(function (event)
        {
          self.resumeRefreshMemory(event);
        });
      });
    });
    const $info = this.$('small[data-role="rf-memory-info"]');
    const content = $info.html();
    const newContent = content + ' （已暂停）';
    $info.html(newContent);
  }

  resumeRefreshMemory(event)
  {
    const $button = $(event.currentTarget);
    $button.prop('disabled', true);
    const $parent = $button.parent();
    this.addTimer('refresh-memory', this.newRefreshMemoryTimer());
    const $pauseButton = $(htmls.get('pause-button'));
    $pauseButton.css('display', 'none');
    const self = this;
    $button.fadeOut(150, function()
    {
      $button.remove();
      $parent.append($pauseButton);
      feather.replace();
      $pauseButton.fadeIn(150, function()
      {
        $pauseButton.click(function (event)
        {
          self.pauseRefreshMemory(event);
        });
      });
    });
    const $info = this.$('small[data-role="rf-memory-info"]');
    const content = $info.html();
    const newContent = content.substring(0, content.length - 5);
    $info.html(newContent);
  }

  /* Helper(s) */

  pulseFile($file)
  {
    $file.addClass('is-invalid');
    const $customFile = $file.closest('.custom-file');
    $file.off('blur'); // remove blur event handlers if any
    $file.focus();
    pulse($customFile);
    $file.blur(function (event)
    {
      if ($file.val() === "")
      {
        $(this).removeClass('is-invalid');
      }
    });
  }

  newRefreshMemoryTimer()
  {
    return setInterval(() => this.refreshMemory(), 5000);
  }

  showMemoryChart(data)
  {
    this.maxMemory = data.maxMemory;
    this.initialMemory = data.initialMemory;
    const max = data.maxMemory;
    const initial = data.initialMemory;
    const total = data.totalMemory;
    const free = data.freeMemory;
    const used = data.totalMemory - data.freeMemory;
    const ctx = document.getElementById('runtimeChart').getContext('2d');
    this.memoryChart = new Chart(ctx, {
      type: 'doughnut',
      data: {
          labels: [
            '已用',
            '空闲'
          ],
          datasets: [{
            data: [
              used,
              free
            ],
            backgroundColor: [
              chartColors.red,
              chartColors.blue
            ],
          }]
      },
      options: {
        responsive: true,
        legend: {
          position: 'top',
        },
        title: {
          display: true,
          text: '初始值: ' + Math.round(toMb(initial)) + ' MB,' +
                ' 当前值: ' + Math.round(toMb(total)) + ' MB,' +
                ' 最大值: ' + Math.round(toMb(max)) + ' MB'
        },
        tooltips: {
          callbacks: {
            label: function(tooltipItem, data)
            {
              let label = data.labels[tooltipItem.index];
              label += ': ';
              const value = data.datasets[0].data[tooltipItem.index];
              const percentage = toPercentage(value, total);
              label += Math.round(toMb(value)) + ' MB ' + '(' + percentage + '%)';
              return label;
            }
          }
        },
        animation: {
          animateScale: true,
          animateRotate: true
        }
      }
    });
  }

  // Uses random number to demonstrate refreshing
  refreshMemory()
  {
    this.freeMemory = Math.floor(Math.random() * 1073741824);
    this.memoryChart.data.datasets[0].data[0] = 1073741824 - this.freeMemory;
    this.memoryChart.data.datasets[0].data[1] = this.freeMemory;
    this.memoryChart.update();
  }

  addTimer(name, timer)
  {
    if (this.timers[name])
    {
      throw 'Timer name \'' + name + '\' already exist';
    }
    else
    {
      this.timers[name] = timer;
    }
  }

  removeTimer(name)
  {
    if (this.timers[name])
    {
      const timer = this.timers[name];
      clearInterval(timer);
      this.timers[name] = null;
    }
  }

  cleanup()
  {
    // timers
    for (let name in this.timers)
    {
      this.removeTimer(name);
    }
  }

  delegateStartApp($button)
  {
    const self = this;
    $button.click(function (event)
    {
      self.startApp(event);
    });
  }

  delegateStopApp($button)
  {
    const self = this;
    $button.click(function (event)
    {
      self.stopApp(event);
    });
  }

  delegateCommands($tbody)
  {
    const self = this;
    $tbody.find('button[data-role="start-app"]').click(function (event)
    {
      self.startApp(event);
    });
    $tbody.find('button[data-role="stop-app"]').click(function (event)
    {
      self.stopApp(event);
    });
    $tbody.find('button[data-role="reload-app"]').click(function (event)
    {
      self.reloadApp(event);
    });
    $tbody.find('button[data-role="remove-app"]').click(function (event)
    {
      self.removeApp(event);
    });
    $tbody.find('button[data-role="expire-sessions"]').click(function (event)
    {
      self.expireSessions(event);
    });
  }

  updateAppInfo($tr, app)
  {
    this.updateAppPath($tr, app);
    this.updateAppName($tr, app);
    this.updateAppState($tr, app);
    this.updateAppSessionNum($tr, app);
  }

  resetToStopButton($tr, app)
  {
    const $startButton = $tr.find('button[data-role="start-app"]');
    if ($startButton.length > 0)
    {
      const $parent = $startButton.parent();
      const stopButtonTmpl = _.template(stopButtonTemplate);
      const $stopButton = $(stopButtonTmpl({app: app}));
      const self = this;
      $stopButton.css('display', 'none');
      $startButton.fadeOut(400, function ()
      {
        $startButton.remove();
        $parent.prepend($stopButton);
        $stopButton.fadeIn(400, function ()
        {
          self.delegateStopApp($stopButton);
        });
      });
    }
  }

  updateAppPath($tr, app)
  {
    const $div = $tr.find('div[data-role="display-path"]');
    const $td = $div.parent();
    const path = app.path;
    const $newDiv = $('<div data-role="display-path"><a href="/' + path + '" target="_blank">/' + path + '</a></div>');
    transitionEls($td, $div, $newDiv);
  }

  updateAppName($tr, app)
  {
    const $div = $tr.find('div[data-role="display-name"]');
    const $td = $div.parent();
    const $newDiv = $('<div data-role="display-name">' + app.name + '</div>');
    transitionEls($td, $div, $newDiv);
  }

  updateAppState($tr, app)
  {
    const $div = $tr.find('div[data-role="display-state"]');
    const $td = $div.parent();
    const $newDiv = $('<div data-role="display-state">' + app.isRunning + '</div>');
    transitionEls($td, $div, $newDiv);
  }

  updateAppSessionNum($tr, app)
  {
    const $div = $tr.find('div[data-role="display-sessions"]');
    const $td = $div.parent();
    const $newDiv = $('<div data-role="display-sessions">' + app.sessionNum + '</div>');
    transitionEls($td, $div, $newDiv);
  }
};

class ReportsView extends BaseChildView
{
  constructor()
  {
    super('reports');
  }

  loadData()
  {
    const deferred = $.Deferred();
    deferred.resolve({
      reports: [ { path: "" }, { path: "test" }, { path: "dev" } ]
    });
    return deferred.promise();
  }

  postRender(data)
  {
  }

  delegateEvents()
  {
    const self = this;
    // report list
    this.$('#rf-reports').click(function (event)
    {
      self.refreshReports(event);
    });
  }

  /* Event handler(s) */

  refreshReports(event)
  {
    const $button = $(event.currentTarget);
    const content = $button.html();
    toLoadingIndicator($button);
    const self = this;
    $.when(loadJson('reports'))
    .done(function (data)
    {
      const $list = self.$('#report-list');
      $list.fadeOut(400, function ()
      {
        const $newList = $('<div id="report-list"></div>');
        $newList.css('display', 'none');
        const reportListTmplFunc = tmplFuncs.get('report-list');
        $newList.html(reportListTmplFunc(data));
        $list.remove();
        self.$el.append($newList);
        $newList.fadeIn(400);
      });
    })
    .fail(function (jqXHR, textStatus, errorThrown)
    {
      if (!handledGlobally(jqXHR.status))
      {
        alert('Failed to load reports:\n    ' + errorThrown);
      }
    })
    .always(function ()
    {
      resetButton($button, content);
    });
  }
}

class UserView extends BaseChildView
{
  constructor()
  {
    super('user');
  }

  loadData()
  {
    const deferred = $.Deferred();
    deferred.resolve({
      username: "admin"
    });
    return deferred.promise();
  }

  postRender(data)
  {
  }

  delegateEvents()
  {
    const self = this;
    // change password
    this.$('#change-password-form').submit(function (event)
    {
      self.changePassword(event);
    });
    // show passwords
    this.$('[data-role="show-password"]').click(function (event)
    {
      self.showPassword(event);
    });
  }

  /* Event handler(s) */

  changePassword(event)
  {
    event.preventDefault();
    const $form = $(event.currentTarget);
    const $password = $form.find('#password');
    const $newPassword = $form.find('#new-password');
    const $newPasswordAgain = $form.find('#new-password-again');
    const $submit = $form.find('#submit');
    const password = $password.val();
    const newPassword = $newPassword.val();
    const newPasswordAgain = $newPasswordAgain.val();
    $password.removeClass('is-invalid');
    $newPassword.removeClass('is-invalid');
    $newPasswordAgain.removeClass('is-invalid');
    unmuteInvalidFeedbacks($form);
    if (password.length == 0)
    {
      invalidAndFocus($password);
      pulse($password.parent());
    }
    else
    {
      if (newPassword.length == 0)
      {
        invalidAndFocus($newPassword);
        pulse($newPassword.parent());
      }
      else
      {
        if (newPasswordAgain.length == 0)
        {
          invalidAndFocus($newPasswordAgain);
          pulse($newPasswordAgain.parent());
        }
        else
        {
          if (newPassword !== newPasswordAgain)
          {
            muteInvalidFeedback($newPassword);
            muteInvalidFeedback($newPasswordAgain);
            alert('两次输入的新密码不一致');
            invalidAndFocus($newPassword);
            invalidate($newPasswordAgain);
            pulse($newPassword.parent());
            pulse($newPasswordAgain.parent());
            $newPassword.val('');
            $newPasswordAgain.val('');
          }
          else
          {
            this.doChangePassword(password, newPassword, newPasswordAgain, $password, $newPassword, $newPasswordAgain, $submit);
          }
        }
      }
    }
  }

  showPassword(event)
  {
    const $toggle = $(event.currentTarget);
    const $container = $toggle.parent();
    const $input = $container.find('input');
    $input.prop('type', 'text');
    $toggle.remove();
    $container.append(htmls.get('hide-password'));
    feather.replace();
    this.delegateHidePassword($container);
  }

  hidePassword(event)
  {
    const $toggle = $(event.currentTarget);
    const $container = $toggle.parent();
    const $input = $container.find('input');
    $input.prop('type', 'password');
    $toggle.remove();
    $container.append(htmls.get('show-password'));
    feather.replace();
    this.delegateShowPassword($container);
  }

  /* Helper(s) */
  
  doChangePassword(password, newPassword, newPasswordAgain, $password, $newPassword, $newPasswordAgain, $submit)
  {
    const data = {
      password: password,
      newPassword: newPassword,
      newPasswordAgain: newPasswordAgain,
    };
    const content = $submit.html();
    toLoadingIndicator($submit);
    $.when(sendJson('change-password', data))
    .done(function (data, textStatus, jqXHR)
    {
      mainView.childView.logout();
      window.history.pushState(null, '', window.location.pathname + '#');
    })
    .fail(function (jqXHR, textStatus, errorThrown)
    {
      if (!handledGlobally(jqXHR.status))
      {
        if (jqXHR.responseText)
        {
          alert(jqXHR.responseText);
        }
        else
        {
          alert('Failed to change password:\n    ' + errorThrown);
        }
        $password.val('');
        $newPassword.val('');
        $newPasswordAgain.val('');
        $password.focus();
      }
    })
    .always(function ()
    {
      resetButton($submit, content);
    });
  }

  delegateHidePassword($container)
  {
    const $toggle = $container.find('svg');
    const self = this;
    $toggle.click(function (event)
    {
      self.hidePassword(event);
    });
  }

  delegateShowPassword($container)
  {
    const $toggle = $container.find('svg');
    const self = this;
    $toggle.click(function (event)
    {
      self.showPassword(event);
    });
  }
}

class NotFoundView extends BaseChildView
{
  constructor()
  {
    super('not-found');
  }
}


// Security ------------------------------------------------------------------------------------------------------------------------------------------------------------------

// Just for demo
const security = {
  
  user: null,

  logout: function ()
  {
    this.cleanup();
  },

  cleanup: function ()
  {
    this.user = null;
  }
};


// Global Helpers ------------------------------------------------------------------------------------------------------------------------------------------------------------

function invalidAndFocus($input)
{
  invalidate($input);
  $input.focus();
}

function invalidate($input)
{
  $input.addClass('is-invalid');
}

function muteInvalidFeedback($input)
{
  const $parent = $input.parent();
  const $invalidFeedback = $parent.find('.invalid-feedback');
  $invalidFeedback.removeClass('invalid-feedback').addClass('invalid-feedback-muted');
}

function unmuteInvalidFeedbacks($form)
{
  const $els = $form.find('.invalid-feedback-muted');
  $els.removeClass('invalid-feedback-muted').addClass('invalid-feedback');
}

function pulse($el, callback)
{
  $el.animate( { opacity: 0 }, 'fast', function ()
  {
    $el.animate( { opacity: 1 }, 'fast', function ()
    {
      $el.animate( { opacity: 0 }, 'fast', function ()
      {
        $el.animate( { opacity: 1 }, 'fast', function ()
        {
          $el.animate( { opacity: 0 }, 'fast', function ()
          {
            $el.animate( { opacity: 1 }, 'fast', function ()
            {
              if (callback)
              {
                callback();
              }
            });
          });
        });
      });
    });
  });
}

function transitionEls($parent, $old, $new)
{
  $new.css('display', 'none');
  $old.fadeOut(400, function ()
  {
    $old.remove();
    $parent.append($new);
    $new.fadeIn();
  });
}

function toLoadingIndicator($button)
{
  $button.css('width', $button.css('width')); // preserve width
  $button.empty().append(htmls.get('loading-indicator'));
  $button.prop('disabled', true);
}

function resetButton($button, content)
{
  $button.css('width', ''); // reset width
  $button.empty().append(content);
  $button.prop('disabled', false);
}

function appendLoadingIndicator($el)
{
  $el.append(htmls.get('loading-indicator'));
  $el.prop('disabled', true);
}

function resetElement($el)
{
  $el.find('i').remove();
  $el.prop('disabled', false);
}

function toMb(byteNum)
{
  return byteNum / 1024 / 1024;
}

function toPercentage(numerator, denominator)
{
  return Math.round(numerator / denominator * 100);
}

function getHash()
{
  const hash = window.location.hash;
  return hash ? hash.substring(1) : '';
}

function getErrorView()
{
  if (!errorView)
  {
    errorView = new ErrorView();
    return errorView;
  }
  else
  {
    return errorView;
  }
}

// Used for UX testing (e.g. Loading effect)
// Usage: $.when(action()..., delay(time)).done(function (...)  {...});
function delay(time)
{
  const deferred = $.Deferred();
  _.delay(function ()
  {
    deferred.resolve();
  }, time);
  return deferred.promise();
}


// Start ---------------------------------------------------------------------------------------------------------------------------------------------------------------------

$.when(delay(1000)).done(function ()
{
  $('#app-loading').fadeOut (function ()
  {
    if (security.user)
    {
      mainView.transitionTo(new ManagerView(), user);
    }
    else
    {
      const hash = getHash();
      if (hash.length > 0)
      {
        router.lastRequestedFragment = hash;
        alert('请先登录');
      }
      const loginView = new LoginView();
      mainView.transitionTo(loginView);
      window.history.pushState(null, '', window.location.pathname + '#');
    }
  });

  addEventListener('hashchange', handleHashChange);
});


})();